本文同步發布於個人部落格
這是一個有點嚴肅的話題 (並沒有)。
好啦,事情是這樣的。
已知前端工程師的職責就是把畫面美美地呈現給使用者,多數人就會覺得我們最主要是在刻畫面。
誒... 不能說錯,但真的寫畫面結構的時間可能佔開發時間裡的少部分,畢竟你懂得,嗯,現代框架 XD
其實大部分時間前端反而是把時間花在跟資料的對話上。
這裡不討論純靜態的網站,實務上大部分上線的網站,其資料基本都是來自後端的 api。
那就有趣了,前後端分離的情況下,後端規劃的 api response 結構長怎樣會很大幅度影響前端的開發。
如果後端的結構相當複雜,前端往往會需要花一些時間去處理資料。
相對的,前端處理使用者的 input 也會需要把這些資料轉成 api 可以吃進去的格式,也會再花一些時間。
不過,當然,實務上前後端各自的系統架構師會盡量提早溝通好,盡量避免這種前後端不協調的情況發生。
但也總是會有一些不得已的情況,比如日期的格式,就有可能在前後端各自的框架裡有不一致的需求,這時很難要求任一方去改,因為那是框架問題。
所以前端收到資料後對資料做處理是無可避免的。
而且以實務情況來說,前端開發常常會拆分各種可複用的元件,往往會為元件定義好需要的 props 類型。
但在使用方,不見得原始資料就一定符合這些 props 類型,這時也需要前端做一些資料轉換。
所以前端工程師其實工作很大一部分是在處理各種資料的轉換。
所以這篇就是想稍微說說前端在處理資料時的一些眉眉角角。
我們都知道 object 的拷貝有兩種寫法:
// 淺拷貝
const shallowCopy = { ...originalObject }
// 深拷貝
const deepCopy = JSON.parse(JSON.stringify(originalObject)) // 注意:這種深拷貝有其限制,僅適用於純資料物件
const deepCopy = structuredClone(originalObject) // 近年新出現的深拷貝方法,支援更多資料類型
淺拷貝雖然也是前端常用的手法,但它有一個問題,就是物件的傳址特性。
如果我們對原資料做淺拷貝後就去做資料處理,其實是會影響到原資料的!
如果這時剛好有其他地方也在使用原資料,可能就會造成意想不到的 bug。
所以實務上,在真的要處理資料前,一般是用深拷貝的方式來複製一份資料,然後才對複製的資料做處理。
淺拷貝的應用基本都是出現在深拷貝複製完資料之後的應用,或是很確定動到原始資料也沒關係的情況下才會使用。
一些框架也會提供自己的深拷貝 method,比如 Quasar 的 extend()
,提供了用更簡單、便捷的語法來做深拷貝。
這也側面顯示了深拷貝在前端開發中的重要性。
其實深拷貝在測試中也很常用到。
因為在寫測試時,通常都不會實際 call api 或是填 form,往往都是直接用一份 mock 的資料來做測試。
在很多測試項目時,如果用淺拷貝就可能發生前一個測試改動了 mock 資料,進而影響了後一個測試的結果。
為了測試的獨立性,通常也會用深拷貝來確保每個測試都不會互相影響。
淺拷貝與深拷貝是 for object 的方式,但在實務上 array 也是相當常處理的資料結構。
甚至很多時候 array 都是跟 object 一起出現。
在操縱 array 的諸多 method 裡,我相信大家在學 JavaScript 時一定也有被提醒有些會影響到原陣列,有些不會。
可能剛學時沒太多想法,但實務上這個差異是很重要的。
在多人協作裡,通常會建議使用不改變原陣列的方法,以方便追蹤 state 流向,避免難以預測的 side effect。
比如 push()
、pop()
、shift()
、unshift()
、splice()
等等,這些都是會改變原陣列的。
而 concat()
、slice()
、map()
、filter()
、reduce()
等等,這些都是不會改變原陣列的。
最常搞混的就是 slice
跟 splice
,因為他們真的長太像了。
這裡用 Vue 來稍微簡單舉一個前後端資料交換的例子。
情境是前端有一個表單,使用者可以填寫資料,然後送出到後端。
然後這個表單同時也會用作編輯,所以也會從後端那裡 get 到已有資料來填充表單。
一般來說前端會建好一個 initialForm 物件,定義好空表單初始的樣子。
然後在表單元件初始化時,透過深拷貝將 initialForm 複製一份,然後再用這份複製的資料來填充表單。
const form = ref(deepCopy(initialForm)) // 假設特別寫了一支專門做深拷貝的函式
用深拷貝複製一份的用處是確保 initialForm 永遠都保持最乾淨的樣子,任何操作都不會影響到它。
由此保障每次使用此元件時都能有一個乾淨的表單初始狀態。
那我們在將 form 資料 POST 給後端時,做資料轉換時就可以很放心直接對 form 做處理,不用擔心會影響到 initialForm。
async function submit () {
try {
const payload = formatData(form.value) // 假設有個函式專門處理資料格式轉換
await api.post('/submit', payload)
} catch (error) {
console.error('Submit failed:', error)
}
}
對於編輯時,已知後端會回傳一份資料來填充表單,這時也會用深拷貝來把 response 的欄位資料複製到 form 裡。
然後才做資料轉換,確保 form 裡的資料格式符合前端的需求。
async function fetchData () {
try {
const response = await api.get('/data')
form.value = deepCopy(response.data) // 深拷貝 response 資料到 form
convertFormData(form.value) // 假設有個函式專門處理後端資料格式轉前端格式
} catch (error) {
console.error('Fetch data failed:', error)
}
}